﻿using System;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using System.Collections.Generic;
using System.Linq;
using System.Diagnostics;

namespace Numbers.Transactions
{
    /// <summary>
    /// Manage a transaction of the various values. There is only one of these at any one time!
    /// </summary>
    public class Manager
    {
        /// <summary>
        /// Resource management to make sure an obj joints/leaves a transaction correctly.
        /// </summary>
        public class Joiner : IDisposable
        {
            IObjectInTransaction _obj;
            Manager _mgr;

            double _newValue, _oldValue;

            public Joiner(Manager mgr, IObjectInTransaction obj, double newValue, double oldValue)
            {
                _obj = obj;
                _mgr = mgr;
                _newValue = newValue;
                _oldValue = oldValue;
                if (_mgr != null)
                {
                    Good = mgr.Join(_obj, newValue, oldValue);
                }
                else
                {
                    Good = true;
                }
            }

            public void Dispose()
            {
                if (_mgr != null)
                {
                    _mgr.Done(_obj);
                }
                else
                {
                    /// In case we aren't in a transaction, just short-circuit and pretend!
                    _obj.TransactionOver(true, _newValue, _oldValue, _newValue != _oldValue);
                }
            }

            public bool Good { get; private set; }
        }

        /// <summary>
        /// Returns true if we are currently in a transaction. Mostly for testing.
        /// </summary>
        public bool InTransaction
        {
            get { return TransactionParticipants.Count != 0; }
        }

        /// <summary>
        /// Track what we are doing in this update.
        /// </summary>
        private class ObjectInTransactionInfo
        {
            public IObjectInTransaction _obj;
            public double _oldVal;
            public double _newVal;
            public int _count;
        }

        /// <summary>
        /// Keep track of everyone in the transaction currently.
        /// </summary>
        private Dictionary<IObjectInTransaction, ObjectInTransactionInfo> TransactionParticipants = new Dictionary<IObjectInTransaction, ObjectInTransactionInfo>();

        private List<ObjectInTransactionInfo> TransactionExParticipants = new List<ObjectInTransactionInfo>();

        /// <summary>
        /// State of the current transaction
        /// </summary>
        private bool TransactionIsGood;

        /// <summary>
        /// The first object in any transaction. if bad, we need it to re-raise the prop
        /// changed event to get anything UI hook ups done backwards.
        /// </summary>
        private IObjectInTransaction FirstObject;

        /// <summary>
        /// Called when an object wants to join in a transaction. If it is already a member, check to
        /// see if we are going to mess with what we have - in which case we will fail this guy!
        /// </summary>
        /// <param name="obj"></param>
        /// <returns>True if this is a good join, false otherwise</returns>
        private bool Join(IObjectInTransaction obj, double newValue, double oldValue)
        {
            ///
            /// First case: this guy is already a member of this transaction
            /// 

            ObjectInTransactionInfo info;
            if (TransactionParticipants.TryGetValue(obj, out info))
            {
                info._count += 1;
                if (!TransactionIsGood)
                {
                    return false;
                }
                TransactionIsGood = CloseTo(newValue, info._newVal);
                if (!TransactionIsGood)
                {
                    Logger.LogMessage("Failed (" + obj.Name + ") due to inconsistent values (" + newValue + ", " + info._newVal + ")");
                }
                return TransactionIsGood;
            }

            ///
            /// Is this the start of a new transaction?
            /// 

            if (TransactionParticipants.Count == 0)
            {
                TransactionIsGood = true;
                FirstObject = obj;
            }

            ///
            /// Second case - these guys are now joining this transaction for the first time.
            /// 

            info = new ObjectInTransactionInfo();
            info._obj = obj;
            info._newVal = newValue;
            info._oldVal = oldValue;
            info._count = 1;
            TransactionParticipants[obj] = info;

            return true;
        }

        /// <summary>
        /// Sees if the two values are close to each other (<0.01% difference).
        /// </summary>
        /// <param name="newValue"></param>
        /// <param name="p"></param>
        /// <returns></returns>
        private bool CloseTo(double v1, double v2)
        {
            double delta = Math.Abs(v1 - v2);
            if (v1 != 0)
            {
                delta /= v1;
            }
            return delta < 0.0001;
        }

        /// <summary>
        /// Called when the object is done with the transaction
        /// </summary>
        /// <param name="obj"></param>
        private void Done(IObjectInTransaction obj)
        {
            ObjectInTransactionInfo info;
            if (!TransactionParticipants.TryGetValue(obj, out info))
            {
                throw new InvalidOperationException("Object that is not a member of the transaction is trying to leave the transaction: " + obj.ToString());
            }

            ///
            /// Has this object finished in this transaction? If so, add him to the "acceptable list". Remove
            /// duplicates as it is totally possible that a single guy will come in more than once.
            /// 

            info._count -= 1;
            if (info._count == 0)
            {
                var repeats = (from i in TransactionExParticipants
                               where i._obj == obj
                               select i).FirstOrDefault();
                if (repeats != null)
                {
                    repeats._newVal = info._newVal;
                }
                else
                {
                    TransactionExParticipants.Add(info);
                }

                TransactionParticipants.Remove(obj);
            }

            ///
            /// Finally, is this transaction done? If so -- issue the "accepts!" :-)
            /// 

            if (TransactionParticipants.Count == 0)
            {
                AcceptRejectTransaction();
            }
        }

        /// <summary>
        /// Makes callbacks to all the objects telling them what the result was and what they should do about it.
        /// </summary>
        private void AcceptRejectTransaction()
        {
            foreach (var o in TransactionExParticipants)
            {
                o._obj.TransactionOver(TransactionIsGood, o._newVal, o._oldVal, TransactionIsGood || (o._obj == FirstObject));
            }

            ///
            /// Clean up!
            /// 

            TransactionExParticipants.Clear();
            TransactionIsGood = true;
            FirstObject = null;

            ///
            /// If anyone is watching this transaction, make sure they get called! This gets tricky: if someone throws!
            /// 

            Exception bad = null;
            foreach (var k in TransactionDoneCallbacks.Keys)
            {
                try
                {
                    TransactionDoneCallbacks[k]();
                }
                catch (Exception e)
                {
                    bad = e;
                }
            }
            TransactionDoneCallbacks.Clear();
            if (bad != null)
            {
                throw bad;
            }
        }

        /// <summary>
        /// Force the current transaction to fail.
        /// </summary>
        public void ForceFail()
        {
            if (TransactionParticipants.Count == 0)
            {
                throw new InvalidOperationException("Attempt to force fail a transaction when no transaction is in progress");
            }
            TransactionIsGood = false;
        }

        /// <summary>
        /// Keep track of any callbacks we need to make so folks watching this transaction can
        /// be informed.
        /// </summary>
        private Dictionary<object, Action> TransactionDoneCallbacks = new Dictionary<object, Action>();

        /// <summary>
        /// Wait for the transactoin to complete, and then run this particular call-bck when it does.
        /// </summary>
        /// <param name="who"></param>
        /// <param name="callback"></param>
        public void SignupForTransactionDone(object who, Action callback)
        {
            if (!InTransaction && TransactionExParticipants.Count == 0)
                throw new InvalidOperationException("No transaction in progress - can't be notified when it is done!");

            TransactionDoneCallbacks[who] = callback;
        }

        /// <summary>
        /// Keeps track of all the values that we are managing.
        /// </summary>
        private List<Value> _managedValues = new List<Value>();

        /// <summary>
        /// Ok - we are going to assoicate several of these guys with each other!
        /// We are pretty simplistic here - just go through and add to our internal list,
        /// and then kill off the other manager's internal list.
        /// </summary>
        /// <param name="v2"></param>
        public void ManageValue(Value v2)
        {
            if (_managedValues.Contains(v2))
            {
                return;
            }


            if (v2.TransactionManager != null)
            {
                Manager oldManager = v2.TransactionManager;

                ///
                /// Add their list to ours
                /// 

                oldManager._managedValues.ForEach(v => InternalAddManagedValue(v));

                ///
                /// Clear out their list - though at this point I hope that other
                /// transaction manager has been orphaned!
                /// 

                oldManager._managedValues.Clear();

            }

            ///
            /// Make sure this guy is added now
            /// 

            InternalAddManagedValue(v2);
        }

        /// <summary>
        /// Internal adder - make sure we don't have, and make sure the links are right!
        /// </summary>
        /// <param name="v"></param>
        private void InternalAddManagedValue(Value v)
        {
            if (!_managedValues.Contains(v))
            {
                _managedValues.Add(v);
                v.TransactionManager = this;
            }
        }

        /// <summary>
        /// Get everyone under a single manager. Use an existing manager, or create a new one
        /// if there is none.
        /// </summary>
        /// <param name="value"></param>
        public static void ManageByOneManager(Value[] values)
        {
            var alltm = from v in values
                        where v.TransactionManager != null
                        select v.TransactionManager;
            Manager m = alltm.FirstOrDefault();
            if (m == null)
            {
                m = new Manager();
            }

            foreach (var v in values)
            {
                m.ManageValue(v);
            }

        }
    }
}
